﻿// Class to listen on TCP port and register client's messages in a Database
// <copyright file="Server.cs" company="EasyCompany">
// EasyCompany
// </copyright>
namespace PstBakup2013Server
{
    using System;
    using System.IO;
    using System.Net.Sockets;
    using System.Collections.Generic;

    /// <summary>
    /// This Class implement a TCP server that listen on a port for client's messages.
    /// </summary>
    internal class Server
    {
        internal class Settings
        {
            /// <summary>
            /// Constructor for the class. Initialize <see cref="ClientPort"/> and <see cref="ReportPort"/> to default value (42000 and 43000).
            /// </summary>
            internal Settings()
            {
                ClientsPort = 42000;
                ReportsPort = 43000;
            }

            /// <summary>
            /// Gets or Sets the TCP port on which this server will listen Clients messages.
            /// </summary>
            internal int ClientsPort { get; set; }

            /// <summary>
            /// Gets or Sets the TCP port on which this server will listen Reports requests.
            /// </summary>
            internal int ReportsPort { get; set; }
        }

        /// <summary>
        /// Messages that have been sent by clients to be inserted into the Database.
        /// </summary>
        internal static List<string> clientMessages = new List<string>();

        /// <summary>
        /// Port on which the server listen clients.
        /// </summary>
        private int clientsPort;

        /// <summary>
        /// Port on which the server listen reports requests.
        /// </summary>
        private int reportsPort;

        /// <summary>
        /// Timeout when receiving data from client.
        /// </summary>
        private int clientTimeOut = 15000;

        /// <summary>
        /// Reference to the socket of the server that listen client's messages.
        /// </summary>
        private Socket serverSocketForClients;

        /// <summary>
        /// Reference to the socket of the server that listen report's requests.
        /// </summary>
        private Socket serverSocketForReports;

        /// <summary>
        /// A reference to the thread for SqlUploader which will process messages from this server.
        /// </summary>
        private System.Threading.Thread _threadSqlUploader;

        /// <summary>
        /// A reference to the SqlUploader
        /// </summary>
        private SqlUploader _sqlUploader;

        /// <summary>
        /// Determine whether or not the server is running.
        /// </summary>
        private bool isRunning = false;

        /// <summary>
        /// Reference to the logger
        /// </summary>
        private Logger logger;

        /// <summary>
        /// Initializes a new instance of the <see cref="Server" /> class. Initialize the Server Port.
        /// </summary>
        /// <param name="serverPort">TCP port on which the server will listen.</param>
        internal Server(Settings serverSettings)
        {
            logger = new Logger(@"C:\Windows\Temp\", "PstBackup2013Server.log");
            this.clientsPort = serverSettings.ClientsPort;
            this.reportsPort = serverSettings.ReportsPort;
            this._sqlUploader = new SqlUploader("sql-test", "PstBackup2013", String.Empty, String.Empty);
            _threadSqlUploader = new System.Threading.Thread(new System.Threading.ThreadStart(_sqlUploader.Start));
            _threadSqlUploader.IsBackground = false;
            _threadSqlUploader.Start();
        }

        ~Server()
        {
            try
            {
                this._sqlUploader.Stop();
                System.Threading.Thread.Sleep(1000);
                this._threadSqlUploader.Join(1000);
                this._threadSqlUploader = null;
            }
            catch (Exception) { }
        }

        /// <summary>
        /// Start to listen on the configured TCP Port. The Server is ready to accept incoming client connection. This method is call when the service start.
        /// </summary>
        internal void Start()
        {
            if (this.serverSocketForClients != null)
            {
                this.Stop();
            }

            if (this.serverSocketForReports != null)
            {
                this.Stop();
            }

            try
            {
                logger.WriteLog("===================================================================================================================================");
                logger.WriteLog("The server is starting...");
                logger.WriteLog("===================================================================================================================================");
                this.serverSocketForClients = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                this.serverSocketForClients.ReceiveBufferSize = 4096;
                this.serverSocketForClients.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Any, (int)this.clientsPort));
                this.serverSocketForClients.Listen(4);

                this.serverSocketForReports = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                this.serverSocketForReports.ReceiveBufferSize = 4096;
                this.serverSocketForReports.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Any, (int)this.reportsPort));
                this.serverSocketForReports.Listen(4);

                this.isRunning = true;
                logger.WriteLog("The server is running and listening clients on port : " + this.clientsPort.ToString());
                logger.WriteLog("The server is running and listening report requests on port : " + this.reportsPort.ToString());
                this.AcceptClient();
                this.AcceptReport();
            }
            catch (SocketException socketEx)
            {
                logger.WriteLog("An error occurred while starting the server : " + socketEx.Message + ". (" + socketEx.SocketErrorCode + ")");
            }
            catch (Exception ex)
            {
                logger.WriteLog("An error occurred while starting the server : " + ex.Message);
            }
        }

        /// <summary>
        /// Close the socket. the Server doesn't accept any clients. Already connected clients are still served in their own thread.
        /// </summary>
        internal void Stop()
        {
            this.isRunning = false;
            logger.WriteLog("Stopping Server...");

            if (this.serverSocketForClients != null)
            {
                try
                {
                    if (this.serverSocketForClients.Connected)
                    {
                        this.serverSocketForClients.Disconnect(false);
                        this.serverSocketForClients.Shutdown(SocketShutdown.Both);
                    }

                    this.serverSocketForClients.Close();
                    logger.WriteLog("Server for Clients is stopped");
                    this.serverSocketForClients = null;
                }
                catch (SocketException socketEx)
                {
                    logger.WriteLog("An error occurred while stopping server : " + socketEx.Message + ". (" + socketEx.SocketErrorCode + ")");
                }
                catch (Exception ex)
                {
                    logger.WriteLog("An error occurred while stopping server : " + ex.Message);
                }
            }

            if (this.serverSocketForReports != null)
            {
                try
                {
                    if (this.serverSocketForReports.Connected)
                    {
                        this.serverSocketForReports.Disconnect(false);
                        this.serverSocketForReports.Shutdown(SocketShutdown.Both);
                    }

                    this.serverSocketForReports.Close();
                    logger.WriteLog("Server for Reports is stopped");
                    this.serverSocketForReports = null;
                }
                catch (SocketException socketEx)
                {
                    logger.WriteLog("An error occurred while stopping server : " + socketEx.Message + ". (" + socketEx.SocketErrorCode + ")");
                }
                catch (Exception ex)
                {
                    logger.WriteLog("An error occurred while stopping server : " + ex.Message);
                }
            }
        }

        /// <summary>
        /// Tell the socket to, asynchronously, wait for incoming client connection.
        /// </summary>
        private void AcceptClient()
        {
            this.serverSocketForClients.BeginAccept(new AsyncCallback(this.OnClientConnect), null);
        }

        /// <summary>
        /// Tell the socket to, asynchronously, wait for incoming report request.
        /// </summary>
        private void AcceptReport()
        {
            this.serverSocketForReports.BeginAccept(new AsyncCallback(this.OnReportRequest), null);
        }

        /// <summary>
        /// Callback for the BeginAccept method. Get a new socket for this new client and wait for incoming Data.
        /// </summary>
        /// <param name="result">Parameters returns by the BeginAccept method.</param>
        private void OnClientConnect(IAsyncResult result)
        {
            if (this.isRunning)
            {
                logger.WriteLog("Incoming connection...");
                this.AcceptClient();
                try
                {
                    Socket clientSocket = this.serverSocketForClients.EndAccept(result);
                    clientSocket.ReceiveBufferSize = 4096;

                    logger.WriteLog("Client " + clientSocket.RemoteEndPoint.ToString() + " is now connected");
                    this.WaitForData(clientSocket);
                }
                catch (SocketException socketEx)
                {
                    logger.WriteLog("An error occurred while receiving a connection from client : " + socketEx.Message + ". (" + socketEx.SocketErrorCode + ")");
                }
                catch (Exception ex)
                {
                    logger.WriteLog("An error occurred while receiving a connection from client : " + ex.Message);
                }
            }
        }

        /// <summary>
        /// Callback for the BeginAccept method. Get a new socket for this new report request and wait for incoming Data.
        /// </summary>
        /// <param name="result">Parameters returns by the BeginAccept method.</param>
        private void OnReportRequest(IAsyncResult result)
        {
            if (this.isRunning)
            {
                logger.WriteLog("Incoming report request...");
                this.AcceptReport();
                try
                {
                    Socket clientSocket = this.serverSocketForReports.EndAccept(result);
                    clientSocket.ReceiveBufferSize = 4096;

                    logger.WriteLog("Client " + clientSocket.RemoteEndPoint.ToString() + " is now connected");
                    this.WaitForReportRequest(clientSocket);
                }
                catch (SocketException socketEx)
                {
                    logger.WriteLog("An error occurred while receiving a connection from client : " + socketEx.Message + ". (" + socketEx.SocketErrorCode + ")");
                }
                catch (Exception ex)
                {
                    logger.WriteLog("An error occurred while receiving a connection from client : " + ex.Message);
                }
            }
        }

        /// <summary>
        /// Wait for incoming client's Data and loop until client disconnection or timeout.
        /// </summary>
        /// <param name="clientSocket">Socket connected to the client.</param>
        private void WaitForData(Socket clientSocket)
        {
            try
            {
                clientSocket.ReceiveTimeout = this.clientTimeOut;
                byte[] buffer = new byte[4096];
                int dataCount;
                string fullMessage = string.Empty;

                do
                {
                    dataCount = clientSocket.Receive(buffer);
                    string message = System.Text.UnicodeEncoding.UTF8.GetString(buffer, 0, dataCount);
                    logger.WriteLog(clientSocket.RemoteEndPoint.ToString() + ";" + System.Threading.Thread.CurrentThread.GetHashCode() + ";" + dataCount.ToString());
                    fullMessage += message;
                }
                while (dataCount != 0);

                try
                {
                    logger.WriteLog("Client " + clientSocket.RemoteEndPoint.ToString() + " has disconnect, closing...");
                    clientSocket.Disconnect(false);
                    clientSocket.Shutdown(SocketShutdown.Both);
                    clientSocket.Close();
                }
                catch (Exception) { }

                this.ParseFullMessage(fullMessage);
            }
            catch (SocketException socketEx)
            {
                if (clientSocket.Connected)
                {
                    if (socketEx.SocketErrorCode == SocketError.TimedOut)
                    {
                        logger.WriteLog("Will disconnect " + clientSocket.RemoteEndPoint.ToString() + " due to timeout.");
                        try
                        {
                            clientSocket.Close();
                            logger.WriteLog(clientSocket.RemoteEndPoint.ToString() + " is now disconnected due to timeout.");
                        }
                        catch (Exception)
                        {
                        }
                    }
                    else
                    {
                        logger.WriteLog("An error occurred while receiving Data from client : " + socketEx.Message + ". (" + socketEx.SocketErrorCode + ")");
                    }
                }
            }
            catch (Exception ex)
            {
                logger.WriteLog("An error occurred while receiving Data from client : " + ex.Message);
            }
        }

        /// <summary>
        /// Wait for incoming report request and loop until client disconnection or timeout.
        /// </summary>
        /// <param name="clientSocket">Socket connected to the client.</param>
        private void WaitForReportRequest(Socket clientSocket)
        {
            try
            {
                clientSocket.ReceiveTimeout = this.clientTimeOut;
                byte[] buffer = new byte[4096];
                int dataCount;
                string fullMessage = string.Empty;

                do
                {
                    dataCount = clientSocket.Receive(buffer);
                    string message = System.Text.UnicodeEncoding.UTF8.GetString(buffer, 0, dataCount);
                    logger.WriteLog(clientSocket.RemoteEndPoint.ToString() + " has sent : " + message);

                    if (message == "?EndOfMessage?")
                    {
                        this.RespondToReportRequest(fullMessage, clientSocket);
                        break;
                    }
                    if (message.EndsWith("?EndOfMessage?"))
                    {
                        fullMessage += message.Substring(0, message.IndexOf("?EndOfMessage?"));
                        this.RespondToReportRequest(fullMessage, clientSocket);
                        break;
                    }

                    logger.WriteLog(clientSocket.RemoteEndPoint.ToString() + ";" + System.Threading.Thread.CurrentThread.GetHashCode() + ";" + dataCount.ToString());
                    fullMessage += message;
                }
                while (dataCount != 0);

                try
                {
                    logger.WriteLog("Client " + clientSocket.RemoteEndPoint.ToString() + " has disconnect, closing...");
                    clientSocket.Disconnect(false);
                    clientSocket.Shutdown(SocketShutdown.Both);
                    clientSocket.Close();
                }
                catch (Exception) { }
            }
            catch (SocketException socketEx)
            {
                if (clientSocket.Connected)
                {
                    if (socketEx.SocketErrorCode == SocketError.TimedOut)
                    {
                        logger.WriteLog("Will disconnect " + clientSocket.RemoteEndPoint.ToString() + " due to timeout.");
                        try
                        {
                            clientSocket.Close();
                            logger.WriteLog(clientSocket.RemoteEndPoint.ToString() + " is now disconnected due to timeout.");
                        }
                        catch (Exception) { }
                    }
                    else
                    {
                        logger.WriteLog("An error occurred while receiving Data from client : " + socketEx.Message + ". (" + socketEx.SocketErrorCode + ")");
                    }
                }
            }
            catch (Exception ex)
            {
                logger.WriteLog("An error occurred while receiving Data from client : " + ex.Message);
            }
        }

        /// <summary>
        /// Parse the string into small message. The separator is "?EndOfMessage?".
        /// </summary>
        /// <param name="fullMessage">A string that contains message separate by "?EndOfMessage?".</param>
        private void ParseFullMessage(string fullMessage)
        {
            string[] messages = fullMessage.Split(new string[] { "?EndOfMessage?" }, StringSplitOptions.RemoveEmptyEntries);

            logger.WriteLog(">>>> Found " + messages.Length + " message(s).");
            int i = 0;

            foreach (string message in messages)
            {
                i++;
                logger.WriteLog(">>>> Message " + i.ToString() + " vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\r\n" + message);
                lock (clientMessages)
                {
                    clientMessages.Add(message);
                }
            }

            logger.WriteLog(">>>> No more message ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n");
        }

        private void RespondToReportRequest(string request, Socket clientSocket)
        {
            if (clientSocket.Connected)
            {
                try
                {
                    logger.WriteLog(clientSocket.RemoteEndPoint.ToString() + " has request : " + request);
                    byte[] answer = this.GetReport(request);
                    clientSocket.Send(answer);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }
        }


        private byte[] GetReport(string strRequest)
        {
            byte[] answer = System.Text.UnicodeEncoding.UTF8.GetBytes("UnkownReportRequest");

            RequestParser parser = new RequestParser();
            RequestParser.Request request = parser.ParseRequest(strRequest);

            switch (request.Command)
            {
                case "GetAllClientsList":
                    answer = System.Text.UnicodeEncoding.UTF8.GetBytes(this._sqlUploader.GetAllClientsList());
                    break;
                case "GetAllPstFilesForClientID":
                    string allPstFile = String.Empty;
                    foreach (PstFile pstFile in this._sqlUploader.GetAllPstFilesForClient(request.Parameter))
                    {
                        allPstFile += pstFile.GetPropertiesValues() + "\r\n";
                    }
                    answer = System.Text.UnicodeEncoding.UTF8.GetBytes(allPstFile);
                    break;
                case "GetAllDestinations":
                    List<String> destinations = this._sqlUploader.GetAllDestinations();
                    string allDestinations = String.Empty;
                    foreach (String destination in destinations)
                    {
                        allDestinations += destination + "\r\n";
                    }
                    answer = System.Text.UnicodeEncoding.UTF8.GetBytes(allDestinations);
                    break;
                case "GetAllVersions":
                    List<String> versions = this._sqlUploader.GetAllVersions();
                    string allVersions = String.Empty;
                    foreach (String version in versions)
                    {
                        allVersions += version + "\r\n";
                    }
                    answer = System.Text.UnicodeEncoding.UTF8.GetBytes(allVersions);
                    break;
                case "GetAllClientsForVersion":
                    answer = System.Text.UnicodeEncoding.UTF8.GetBytes(this._sqlUploader.GetAllClientsForVersion(request.Parameter));
                    break;
                case "GetAllClientsForDestination":
                    answer = System.Text.UnicodeEncoding.UTF8.GetBytes(this._sqlUploader.GetAllClientsForDestination(request.Parameter));
                    break;
                case "GetResultIDs":
                    answer = System.Text.UnicodeEncoding.UTF8.GetBytes(this._sqlUploader.GetResultIDs());
                    break;
                default:
                    logger.WriteLog("Cannot generate report for the unkown command : " + request.Command);
                    break;
            }

            return answer;
        }
    }
}
